home *** CD-ROM | disk | FTP | other *** search
/ The Fatted Calf / The Fatted Calf.iso / Applications / Developer / StopWatch / Source / Invoice.m < prev    next >
Encoding:
Text File  |  1994-02-06  |  15.1 KB  |  753 lines

  1. /*
  2.  * For legal stuff see the file COPYRIGHT
  3.  */
  4. #import <stdio.h>
  5. #import <time.h>
  6. #import <bsd/libc.h>
  7. #import <sys/file.h>
  8. #import <sys/types.h>
  9. #import <sys/stat.h>
  10. #import <objc/NXBundle.h>
  11. #import <appkit/appkit.h>
  12. #import "Controller.h"
  13. #import "Invoice.h"
  14. #import "Session.h"
  15. #import "Expense.h"
  16. #import "Preferences.h"
  17. #import "createPath.h"
  18.  
  19. #define EXPENSE            "Expense"
  20. #define DETAIL            "Detail"
  21. #define INVOICE            "Invoice"
  22. #define TEMPLATE_TYPE        "rtf"
  23.  
  24. #define MONTH_SWITCH_DAY    20
  25.  
  26.  
  27. @interface Invoice(PRIVATE)
  28. - (void)copyTemplateIfNeeded:(const char *)template;
  29. - (FILE *)openTemplate:(const char *)template;
  30. - (FILE *)openOutput:(const char *)path;
  31. - (void)filterExpenses:(FILE *)in to:(FILE *)out;
  32. - (void)filterDetail:(FILE *)in to:(FILE *)out;
  33. - (void)filter:(FILE *)in to:(FILE *)out;
  34. - (const char *)translate:(const char *)token;
  35. - (BOOL)filterBuffer:(char *)ptr to:(FILE *)out
  36.          match:(const char *)target;
  37. - (const char *)templatePath:(const char *)template;
  38. - (const char *)createFilename:(const char *)type;
  39. - (int)nextInvoiceNumber;
  40. - generateInvoice;
  41. - generateDetail;
  42. - generateExpenses;
  43. - (int)likelyMonth:(struct tm *)tm;
  44.  
  45. - (const char *)number;        /* invoice number */
  46. - (const char *)today;        /* today's date */
  47. - (const char *)thisMonth;    /* the likely current invoicing month */
  48. - (const char *)contact;    /* person at client */
  49. - (const char *)client;        /* client name */
  50. - (const char *)street;        /* street address */
  51. - (const char *)city;
  52. - (const char *)state;
  53. - (const char *)zip;
  54. - (const char *)enddate;    /* end of invoice period */
  55. - (const char *)startdate;    /* start of invoice period */
  56. - (const char *)billings;    /* total hourly billings */
  57. - (const char *)expenses;    /* total expenses */
  58. - (const char *)total;        /* billings + expenses */
  59. - (const char *)hours;        /* total hours worked for invoice period */
  60. - (const char *)rate;        /* hourly rate */
  61. - (const char *)date;        /* date of a session or expense */
  62. - (const char *)length;        /* length of a session (in hours) */
  63. - (const char *)amount;        /* expense amount */
  64. - (const char *)description;    /* description of session task or expense */
  65.  
  66. - (const char *)myName;        /* consultant's personal information, from prefs */
  67. - (const char *)myCompany;
  68. - (const char *)myStreet;
  69. - (const char *)myCity;
  70. - (const char *)myState;
  71. - (const char *)myZip;
  72. - (const char *)myPhone;
  73. - (const char *)myFax;
  74. - (const char *)myEmail;
  75. @end
  76.  
  77.  
  78. @implementation Invoice
  79.  
  80.  
  81. - initTemplateDir:(const char *)dir
  82. {
  83.   [super init];
  84.  
  85.   templateDir = NXCopyStringBuffer(dir);
  86.  
  87.   if ( createPath( templateDir, DIRMODE ) != PathCreationOk ) {
  88.     NXRunAlertPanel( [NXApp name], "Cannot create path `%s'",
  89.             "Harumph.", NULL, NULL, templateDir );
  90.     return nil;
  91.   }
  92.  
  93.   /*
  94.    * If the templates are not found in the templateDir,
  95.    * copy them there from the app wrapper.
  96.    */
  97.   [self copyTemplateIfNeeded:EXPENSE];
  98.   [self copyTemplateIfNeeded:INVOICE];
  99.   [self copyTemplateIfNeeded:DETAIL];
  100.  
  101.   return self;
  102. }
  103.  
  104. - free
  105. {
  106.   if ( templateDir ) {
  107.     free(templateDir);
  108.     templateDir = NULL;
  109.   }
  110.   return [super free];
  111. }
  112.  
  113. - (BOOL)editTemplate:(const char *)template
  114. {
  115.   const char *path = [self templatePath:template];
  116.   return [[Application workspace] openFile:path];
  117. }
  118.  
  119. - (void)generate:(List *)clientList
  120. {
  121.   int i, count = [clientList count];
  122.   int sessionCount, expenseCount;
  123.  
  124.   for ( i = 0; i < count; i++ ) {
  125.  
  126.     info = [clientList objectAt:i];
  127.     sessionCount = [info sessionCount];
  128.     expenseCount = [info expenseCount];
  129.  
  130.     /* Don't generate an invoice if no work was done */
  131.     if ( sessionCount == 0 && expenseCount == 0 )
  132.       continue;
  133.  
  134.     /* get (and increment) the invoice number */ 
  135.     number = [self nextInvoiceNumber];
  136.  
  137.     [self generateInvoice];
  138.  
  139.     if ( sessionCount > 0 )
  140.       [self generateDetail];
  141.  
  142.     if ( expenseCount > 0 )
  143.       [self generateExpenses];
  144.   }
  145. }
  146.  
  147. @end
  148.  
  149.  
  150. @implementation Invoice(PRIVATE)
  151.  
  152. static char *Tokens[] = {
  153.   "number",
  154.   "today",
  155.   "contact",
  156.   "client",
  157.   "street",
  158.   "city",
  159.   "state",
  160.   "zip",
  161.   "startdate",
  162.   "enddate",
  163.   "billings",
  164.   "expenses",
  165.   "total",
  166.   "hours",
  167.   "rate",
  168.   "date",
  169.   "length",
  170.   "amount",
  171.   "description",
  172.   "myName",
  173.   "myCompany",
  174.   "myStreet",
  175.   "myCity",
  176.   "myState",
  177.   "myZip",
  178.   "myPhone",
  179.   "myFax",
  180.   "myEmail",
  181.   0,
  182. };
  183.  
  184. #define    START        '['
  185. #define    END        ']'
  186. #define MAX_LINE    500
  187. #define MAX_TOKEN    20
  188.  
  189. /*
  190.  * Determine the value of the token, or return NULL if
  191.  * not a known token.
  192.  */
  193. - (const char *)translate:(const char *)token
  194. {
  195.   char **ptr;
  196.  
  197.   for ( ptr = Tokens; *ptr; ptr++ ) {
  198.     if ( strcmp( *ptr, token ) == 0 ) {
  199.       SEL sel = sel_getUid(token);
  200.  
  201.       if ( [self respondsTo:sel] )
  202.     return (const char *)[self perform:sel];
  203.       else
  204.     return NULL;        /* found string, but don't respond */
  205.     }
  206.   }
  207.   return NULL;
  208. }
  209.  
  210. /*
  211.  * Translate the line pointed to by 'ptr', writing the result to the
  212.  * stream 'out'.  Return 1 if the given token occurred in this line.
  213.  */
  214. - (BOOL)filterBuffer:(char *)ptr to:(FILE *)output
  215.          match:(const char *)target
  216. {
  217.   char c, *tok, token[MAX_TOKEN + 1];
  218.   const char *value;
  219.   int count;
  220.   BOOL matched = NO;
  221.  
  222.   for ( ; *ptr ; ptr++ ) {
  223.     if ( *ptr == START ) {
  224.  
  225.       for (count = 0, tok = token;
  226.        count < MAX_TOKEN && (c = *++ptr) && NXIsAlpha(c);
  227.        count++ )
  228.     *tok++ = c;
  229.  
  230.       *tok = '\0';
  231.  
  232.       if ( c == END ) {                        /* got a token */
  233.     if ( (value = [self translate:token]) == NULL )        /* it was unknown */
  234.       fprintf(output, "%c%s%c", START, token, END);        /* pass it thru */
  235.     else {
  236.       fprintf(output, "%s", value);                /* use its value */
  237.       if ( target && strcmp( token, target ) == 0 )
  238.         matched = YES;
  239.     }
  240.       } else if ( !NXIsAlpha(c) )                /* bad character */
  241.     fprintf(output, "%c%s%c", START, token, c);        /* pass it thru */
  242.       else                            /* it was too long */
  243.     fprintf(output, "%c%s", START, token);            /* pass it thru */
  244.     } else
  245.       fputc( *ptr, output );
  246.   }
  247.   return matched;
  248. }
  249.  
  250. /*
  251.  * Straight translation - used to generate invoices.
  252.  */
  253. - (void)filter:(FILE *)input to:(FILE *)output
  254. {
  255.   char buf[MAX_LINE + 1];
  256.  
  257.   while ( fgets( buf, MAX_LINE, input ) )
  258.     [self filterBuffer:buf to:output match:NULL];
  259. }
  260.  
  261. /*
  262.  * Expense generator - loops over Expense records when it
  263.  * finds the {description} token in a line.
  264.  */
  265. - (void)filterExpenses:(FILE *)input to:(FILE *)output
  266. {
  267.   char buf[MAX_LINE + 1];
  268.  
  269.   while ( fgets( buf, MAX_LINE, input ) ) {
  270.     /*
  271.      * When we find {description}, treat the current buf as
  272.      * the template for all expenses.
  273.      */
  274.     if ( [self filterBuffer:buf to:output match:"description"] ) {
  275.       int i, count = [info expenseCount];
  276.  
  277.       for ( i = 0; i < count; i++ ) {
  278.     expense = (Expense *)[info expenseAt:i];
  279.     [self filterBuffer:buf to:output match:NULL];
  280.       }
  281.       expense = nil;
  282.     }
  283.   }
  284. }
  285.  
  286. /*
  287.  * Detail generator - loops over Session records when it
  288.  * finds the {description} token in a line.
  289.  */
  290. - (void)filterDetail:(FILE *)input to:(FILE *)output
  291. {
  292.   char buf[MAX_LINE + 1];
  293.  
  294.   while ( fgets( buf, MAX_LINE, input ) ) {
  295.     /*
  296.      * When we find {description}, treat the current buf as
  297.      * the template for all sessions.
  298.      */
  299.     if ( [self filterBuffer:buf to:output match:"description"] ) {
  300.       int i, count = [info sessionCount];
  301.       
  302.       for ( i = 0; i < count; i++ ) {
  303.     session = (Session *)[info sessionAt:i];
  304.     [self filterBuffer:buf to:output match:NULL];
  305.       }
  306.       session = nil;
  307.     }
  308.   }
  309. }
  310.  
  311. /*
  312.  * Intuit the most likely month that this invoice is for. If it is before
  313.  * the 20th day of the month, assume it is for the prior month, otherwise
  314.  * assume it is for the current month.
  315.  */
  316. - (int)likelyMonth:(struct tm *)tm
  317. {
  318.   int month = tm->tm_mon;
  319.  
  320.   if ( tm->tm_mday < MONTH_SWITCH_DAY )
  321.     month = ( month == 0 ? 11 : month - 1 );    /* special for January */
  322.  
  323.   return month;
  324. }
  325.  
  326. /*
  327.  * Return the (likely) current invoicing month.
  328.  */
  329. - (const char *)thisMonth
  330. {
  331.   time_t now;
  332.   struct tm *tm;
  333.   static char *months[] = {
  334.     "jan", "feb", "mar", "apr",
  335.     "may", "jun", "jul", "aug", "sep",
  336.     "oct", "nov", "dec"
  337.   };
  338.  
  339.   time(&now);
  340.   tm = localtime(&now);
  341.  
  342.   return months[[self likelyMonth:tm]];
  343. }
  344.  
  345. /***********************************************************************
  346.  *             Token Translation Methods
  347.  ***********************************************************************/
  348. - (const char *)number
  349. {
  350.   static char buf[20];
  351.   sprintf( buf, "%d", number );
  352.   return buf;
  353. }
  354.  
  355. /*
  356.  * Return today's date as a string
  357.  */
  358. - (const char *)today
  359. {
  360.   time_t now;
  361.   struct tm *tm;
  362.   static char *months[] = {
  363.     "January", "February", "March", "April",
  364.     "May", "June", "July", "August", "September",
  365.     "October", "November", "December"
  366.   };
  367.   static char buf[80];
  368.  
  369.   time(&now);
  370.   tm = localtime(&now);
  371.   sprintf(buf, "%s %d, 19%d",
  372.       months[tm->tm_mon], tm->tm_mday, tm->tm_year );
  373.   return buf;
  374. }
  375.  
  376. - (const char *)contact
  377. {
  378.   return info ? [info contactName] : "";
  379. }
  380.  
  381. - (const char *)client
  382. {
  383.   return info ? [info clientName] : "";
  384. }
  385.  
  386. - (const char *)street
  387. {
  388.   return info ? [info street] : "";
  389. }
  390.  
  391. - (const char *)city
  392. {
  393.   return info ? [info city] : "";
  394. }
  395.  
  396. - (const char *)state
  397. {
  398.   return info ? [info addrState] : "";
  399. }
  400.  
  401. - (const char *)zip
  402. {
  403.   return info ? [info zipCode] : "";
  404. }
  405.  
  406. - (const char *)enddate
  407. {
  408.   int month;
  409.   time_t now;
  410.   struct tm *tm;
  411.   static char buf[80];
  412.   static int lengths[] = {    /* number of days per month */
  413.     31, 28, 31, 30,        /* gloss over leap years... */
  414.     31, 30, 31, 31,
  415.     30, 31, 30, 31
  416.   };
  417.  
  418.   time(&now);
  419.   tm = localtime(&now);
  420.   month = [self likelyMonth:tm];
  421.  
  422.   sprintf( buf, "%02d/%02d/%02d", month + 1, lengths[month], tm->tm_year );
  423.   return buf;
  424. }
  425.  
  426. /*
  427.  * Intuit the most likely start date for this invoice. If it is before
  428.  * the 20th day of the month, assume it is for the prior month, otherwise
  429.  * assume it is for the current month.
  430.  */
  431. - (const char *)startdate
  432. {
  433.   int month;
  434.   time_t now;
  435.   struct tm *tm;
  436.   static char buf[80];
  437.  
  438.   time(&now);
  439.   tm = localtime(&now);
  440.   month = [self likelyMonth:tm];
  441.  
  442.   sprintf( buf, "%02d/01/%02d", month + 1, tm->tm_year );
  443.   return buf;
  444. }
  445.  
  446. - (const char *)billings
  447. {
  448.   static char buf[20];
  449.   
  450.   if ( info ) {
  451.     commafyDouble( [info totalBillable], buf );
  452.     return buf;
  453.   }
  454.  
  455.   return "";
  456. }
  457.  
  458. - (const char *)expenses
  459. {
  460.   static char buf[20];
  461.   
  462.   if ( info ) {
  463.     commafyDouble( [info totalExpenses], buf );
  464.     return buf;
  465.   }
  466.  
  467.   return "";
  468. }
  469.  
  470. - (const char *)total
  471. {
  472.   static char buf[20];
  473.   
  474.   if ( info ) {
  475.     commafyDouble( [info totalBillable] + [info totalExpenses], buf );
  476.     return buf;
  477.   }
  478.  
  479.   return "";
  480. }
  481.  
  482. - (const char *)hours
  483. {
  484.   static char buf[20];
  485.   
  486.   if ( info ) {
  487.     sprintf( buf, "%1.2f", [info totalHours] );
  488.     return buf;
  489.   }
  490.   return "";
  491. }
  492.  
  493. - (const char *)rate
  494. {
  495.   static char buf[20];
  496.   
  497.   if ( info ) {
  498.     sprintf( buf, "%1.2f", [info hourlyRate] );
  499.     return buf;
  500.   }
  501.   return "";
  502. }
  503.  
  504. - (const char *)date
  505. {
  506.   if ( session )
  507.     return [session startDateString];
  508.  
  509.   if ( expense )
  510.     return [expense dateString];
  511.  
  512.   return "";
  513. }
  514.  
  515. - (const char *)amount
  516. {
  517.   static char buf[20];
  518.  
  519.   if ( !expense )
  520.     return "";
  521.  
  522.   commafyDouble( [expense amount], buf );
  523.   return buf;
  524. }
  525.  
  526. - (const char *)length
  527. {
  528.   static char buf[20];
  529.  
  530.   if ( !session )
  531.     return "";
  532.  
  533.   sprintf( buf, "%1.2f", [session hours] );
  534.   return buf;
  535. }
  536.  
  537. - (const char *)description
  538. {
  539.   if ( expense )
  540.     return [expense description];
  541.  
  542.   if ( session )
  543.     return [session description];
  544.  
  545.   return "";
  546. }
  547.  
  548. /*
  549.  * Consultant information taken from Preferences
  550.  */
  551. - (const char *)myName
  552. {
  553.   return [[Preferences new] myName];
  554. }
  555.  
  556. - (const char *)myCompany
  557. {
  558.   return [[Preferences new] myCompany];
  559. }
  560.  
  561. - (const char *)myStreet
  562. {
  563.   return [[Preferences new] myStreet];
  564. }
  565.  
  566. - (const char *)myCity
  567. {
  568.   return [[Preferences new] myCity];
  569. }
  570.  
  571. - (const char *)myState
  572. {
  573.   return [[Preferences new] myState];
  574. }
  575.  
  576. - (const char *)myZip
  577. {
  578.   return [[Preferences new] myZip];
  579. }
  580.  
  581. - (const char *)myPhone
  582. {
  583.   return [[Preferences new] myPhone];
  584. }
  585.  
  586. - (const char *)myFax
  587. {
  588.   return [[Preferences new] myFax];
  589. }
  590.  
  591. - (const char *)myEmail
  592. {
  593.   return [[Preferences new] myEmail];
  594. }
  595.  
  596.  
  597. - (const char *)templatePath:(const char *)template
  598. {
  599.   static char path[FILENAME_MAX + 1];
  600.   sprintf( path, "%s/%sTemplate.%s", templateDir, template, TEMPLATE_TYPE );
  601.   return path;
  602. }
  603.  
  604. /*
  605.  * If the named template file is not found in the templateDir,
  606.  * copy the one from the app wrapper over there.
  607.  */
  608. - (void)copyTemplateIfNeeded:(const char *)template
  609. {
  610.   FILE *fp;
  611.   const char *templatePath;
  612.   char templateSrc[FILENAME_MAX + 1];
  613.   char name[FILENAME_MAX + 1];
  614.  
  615.   templatePath = [self templatePath:template];
  616.  
  617.   /* assume it's ok if we can open it for reading */
  618.   if ( (fp = fopen( templatePath, "r" )) != NULL ) {
  619.     fclose(fp);
  620.     return;            /* the file is already there */
  621.   }
  622.  
  623.   sprintf( name, "%sTemplate", template );
  624.  
  625.   if ( ! [[NXBundle mainBundle] getPath:templateSrc
  626.         forResource:name ofType:TEMPLATE_TYPE] ) {
  627.     NXRunAlertPanel( [NXApp name], "Can't find %s.%s in app wrapper",    
  628.             "What Bogosity!", NULL, NULL, name, TEMPLATE_TYPE );
  629.     return;
  630.   }
  631.  
  632.   copyFile( templateSrc, templatePath );
  633. }
  634.  
  635. /*
  636.  * Open a template file for reading.
  637.  */
  638. - (FILE *)openTemplate:(const char *)template
  639. {
  640.   FILE *fp;
  641.   const char *path = [self templatePath:template];
  642.  
  643.   if ( ! (fp = fopen( path, "r" )) )
  644.     NXRunAlertPanel( [NXApp name], "Can't open `%s' for reading",
  645.             "Dang!", NULL, NULL, path );
  646.  
  647.   return fp;
  648. }
  649.  
  650. /*
  651.  * Take out blanks and punctuation characters to create a reasonable
  652.  * file name.  NB: Returns a pointer to a local static buffer
  653.  */
  654. - (const char *)createFilename:(const char *)type
  655. {
  656.   char c, *ptr;
  657.   const char *dir = GET_DEFAULT( INVOICE_DIR );
  658.   const char *src = [info shortName];
  659.   static char buf[MAXPATHLEN];
  660.   
  661.   sprintf( buf, "%s/", dir ) ;
  662.   ptr = buf + strlen(buf);
  663.  
  664.   while ( (c = *src++) != '\0' )
  665.     if ( NXIsAlpha(c) )        /* don't copy punctuation */
  666.       *ptr++ = ( NXIsUpper(c) ? NXToLower(c) : c );
  667.  
  668.   sprintf( ptr, ".%d.%s.%s.rtf", number, [self thisMonth], type );
  669.   return buf;
  670. }
  671.  
  672. /*
  673.  * Open a file of the given type for writing
  674.  */
  675. - (FILE *)openOutput:(const char *)type
  676. {
  677.   FILE *fp;
  678.   const char *path = [self createFilename:type];
  679.  
  680.   if ( ! (fp = fopen( path, "w" )) )
  681.     NXRunAlertPanel( [NXApp name], "Can't open `%s' for writing",
  682.             "Drat.", NULL, NULL, path );
  683.  
  684.   return fp;
  685. }
  686.  
  687. /*
  688.  * Read the current "next invoice number" from the defaults database
  689.  * increment and return the number, after storing the value back.
  690.  */
  691. - (int)nextInvoiceNumber
  692. {
  693.   char num[10];
  694.   int value;
  695.  
  696.   value = atoi(GET_DEFAULT( INVOICE_NUM ));
  697.   sprintf( num, "%d", ++value );
  698.   PUT_DEFAULT( INVOICE_NUM, num );
  699.  
  700.   return value;            /* return the incremented value */
  701. }
  702.  
  703. /*
  704.  * Generate an expense report data file for any customer that had
  705.  * expenses for this period.
  706.  */
  707. - generateExpenses
  708. {
  709.   FILE *in, *out;
  710.  
  711.   if ( !( in = [self openTemplate:EXPENSE]) || !( out = [self openOutput:"exp"] ))
  712.     return nil;
  713.  
  714.   [self filterExpenses:in to:out];
  715.  
  716.   fclose( in );
  717.   fclose( out );
  718.  
  719.   return self;
  720. }
  721.  
  722. - generateDetail
  723. {
  724.   FILE *in, *out;
  725.  
  726.   if ( !( in = [self openTemplate:DETAIL]) || !( out = [self openOutput:"dtl"] ))
  727.     return nil;
  728.  
  729.   [self filterDetail:in to:out];
  730.  
  731.   fclose( in );
  732.   fclose( out );
  733.  
  734.   return self;
  735. }
  736.  
  737. - generateInvoice
  738. {
  739.   FILE *in, *out;
  740.  
  741.   if ( !( in = [self openTemplate:INVOICE]) || !( out = [self openOutput:"inv"] ))
  742.     return nil;
  743.  
  744.   [self filter:in to:out];
  745.  
  746.   fclose( in );
  747.   fclose( out );
  748.  
  749.   return self;
  750. }
  751.  
  752. @end
  753.